package org.gist.classloader;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;

import org.apache.log4j.Logger;
import org.gist.classloader.exception.GclException;


/**
 * JarResources reads jar files and loads the class content/bytes in a HashMap
 * 
 * @author Suresh Reddy Guntaka
 * 
 */
public class JarResources {

    protected Map<String, byte[]> jarEntryContents;
    protected boolean collisionAllowed;

    private static Logger logger = Logger.getLogger( JarResources.class );

    /**
     * Default constructor
     */
    public JarResources() {
        jarEntryContents = new HashMap<String, byte[]>();
        collisionAllowed = Configuration.suppressCollisionException();
    }

    /**
     * @param name
     * @return byte[]
     */
    public byte[] getResource(String name) {
        return jarEntryContents.get( name );
    }

    /**
     * Returns an immutable Map of all jar resources
     * 
     * @return Map
     */
    public Map<String, byte[]> getResources() {
        return Collections.unmodifiableMap( jarEntryContents );
    }

    /**
     * Reads the specified jar file
     * 
     * @param jarFile
     */
    public void loadJar(String jarFile) {
        if( logger.isTraceEnabled() )
            logger.trace( "Loading jar: " + jarFile );

        FileInputStream fis = null;
        try {
            fis = new FileInputStream( jarFile );
            loadJar( fis );
        } catch (IOException e) {
            throw new GclException( e );
        } finally {
            if( fis != null )
                try {
                    fis.close();
                } catch (IOException e) {
                    throw new GclException( e );
                }
        }
    }

    /**
     * Reads the jar file from a specified URL
     * 
     * @param url
     */
    public void loadJar(URL url) {
        if( logger.isTraceEnabled() )
            logger.trace( "Loading jar: " + url.toString() );

        InputStream in = null;
        try {
            in = url.openStream();
            loadJar( in );
        } catch (IOException e) {
            throw new GclException( e );
        } finally {
            if( in != null )
                try {
                    in.close();
                } catch (IOException e) {
                    throw new GclException( e );
                }
        }
    }

    /**
     * Load the jar contents from InputStream
     * 
     */
    public void loadJar(InputStream jarStream) {

        BufferedInputStream bis = null;
        JarInputStream jis = null;

        try {
            bis = new BufferedInputStream( jarStream );
            jis = new JarInputStream( bis );

            JarEntry jarEntry = null;
            while (( jarEntry = jis.getNextJarEntry() ) != null) {
                if( logger.isTraceEnabled() )
                    logger.trace( dump( jarEntry ) );

                if( jarEntry.isDirectory() ) {
                    continue;
                }

                if( jarEntryContents.containsKey( jarEntry.getName() ) ) {
                    if( !collisionAllowed )
                        throw new GclException( "Class/Resource " + jarEntry.getName() + " already loaded" );
                    else {
                        if( logger.isTraceEnabled() )
                            logger
                                    .trace( "Class/Resource " + jarEntry.getName()
                                            + " already loaded; ignoring entry..." );
                        continue;
                    }
                }

                if( logger.isTraceEnabled() )
                    logger.trace( "Entry Name: " + jarEntry.getName() + ", " + "Entry Size: " + jarEntry.getSize() );

                byte[] b = new byte[2048];
                ByteArrayOutputStream out = new ByteArrayOutputStream();

                int len = 0;
                while (( len = jis.read( b ) ) > 0) {
                    out.write( b, 0, len );
                }

                // add to internal resource HashMap
                jarEntryContents.put( jarEntry.getName(), out.toByteArray() );
                if( logger.isTraceEnabled() )
                    logger.trace( jarEntry.getName() + ": size=" + out.size() + " ,csize="
                            + jarEntry.getCompressedSize() );

                out.close();
            }
        } catch (IOException e) {
            throw new GclException( e );
        } catch (NullPointerException e) {
            if( logger.isTraceEnabled() )
                logger.trace( "Done loading." );
        } finally {
            if( jis != null )
                try {
                    jis.close();
                } catch (IOException e) {
                    throw new GclException( e );
                }

            if( bis != null )
                try {
                    bis.close();
                } catch (IOException e) {
                    throw new GclException( e );
                }
        }
    }

    /**
     * For debugging
     * 
     * @param je
     * @return String
     */
    private String dump(JarEntry je) {
        StringBuffer sb = new StringBuffer();
        if( je.isDirectory() ) {
            sb.append( "d " );
        } else {
            sb.append( "f " );
        }

        if( je.getMethod() == JarEntry.STORED ) {
            sb.append( "stored   " );
        } else {
            sb.append( "defalted " );
        }

        sb.append( je.getName() );
        sb.append( "\t" );
        sb.append( "" + je.getSize() );
        if( je.getMethod() == JarEntry.DEFLATED ) {
            sb.append( "/" + je.getCompressedSize() );
        }

        return ( sb.toString() );
    }
}
